package AbstractMapper;
import java.util.ArrayList;

import org.opencyc.api.CycObjectFactory;
import org.opencyc.cycobject.CycConstant;
import org.opencyc.cycobject.CycList;
import org.opencyc.cycobject.CycObject;
import org.opencyc.cycobject.CycVariable;
import org.opencyc.util.Log;
import org.apache.commons.math.linear.*;



/**
 *Represents a map between a pair of cyc constants and describes how they relate in terms of abstract category memberships.
 *Currently utilizes the 'genls' and 'isa' relationships from OpenCyc.
 *<p>
 *Scoring:
 *
 *<ul>
 *<li> *totalHits = number of matches between all isa and all genls of two constants 
 *<li>inverseDistance = totalLevels - sum of path length between matches 
 *<li>firstDistance = path length to first match
 *
 *@author Nic Di Noia - nik.ddn@gmail.com
 * 
 *
 */
//TODO ADD SWITCH TO TOGGLE BETWEEN USE OF ISA AND GENLS
public class AbstractElementPair {
	
	private static int totalLevels = 0; //Below Root Node 'Thing' ->114650; // used for inverseDist (I prefer a negative value) //TODO update
	private static int max_int = Integer.MAX_VALUE;
	private CycConnection cycConn;
	private CycConstant c, c1;
	private CycObject l,l1;	
	private CycList matches, firstMatches; 
	private int isaHits, isaInverseDist, firstIsaDist, genlsHits, genlsInverseDist, firstGenlsDist;
	private Double angle;
	private ArrayRealVector v, v1;
	

	/**
	 * Creates an AbstractElementPair used to compare the isa and genls similarities of <b>two</b> constants.
	 * In other words, if constantName and constantName1 share any isa or genls relationships with any other constant,  
	 * this will affect their scores.
	 * 
	 * @param constantName		The first constant to be compared
	 * @param constantName1		The second constant to be compared
	 * @param cycC				The connection to the OpenCyc world
	 *
	 */
	public AbstractElementPair(CycConstant constantName, CycConstant constantName1, CycConnection cycC)
	{
		this.cycConn = cycC;				
		this.c = constantName;
		this.c1 = constantName1;
		//this.init();
		//this.initRel();
	}
	
	@SuppressWarnings("unchecked")
	private void init()
	{
		this.isaHits=0;
		this.isaInverseDist=0;
		this.genlsHits=0;
		this.genlsInverseDist=0;
		this.matches = new CycList();
		this.firstMatches = new CycList();		

		if (c.equals(c1))
		{
			matches.add(c);
			firstMatches.add(c);			
			this.firstIsaDist = 0;
			this.firstGenlsDist=0;
		}
		else
		{
			this.firstIsaDist= AbstractElementPair.max_int;
			this.firstGenlsDist= AbstractElementPair.max_int;
		}
	}
	private void initRel()
	{
		this.angle = 0.0;
		this.v = null;
		this.v1 = null;
		this.l = null;
		this.l1 = null;
	}
	
	protected CycConstant getConstOne()
	{
		return this.c;
	}
	
	protected CycConstant getConstTwo()
	{
		return this.c1;
	}
	
	/**
	 * @return 	the number of constants in common by comparing linearly: 
	 * 				the lists given by the OpenCyc world corresponding to all 
	 * 				Isa relationships of the two constants in this pair.
	 * @see CycConnection#getAllIsa
	 * 				
	 */
	public int getIsaHits()
	{
		return this.isaHits;
	}
	/**
	 * 
	 * @return the sum of the path lengths for every constant found on both lists of all isas.
	 * 
	 */
	public int getIsaInverseDist()
	{
		return this.isaInverseDist;
	}
	
	/**
	 * Calculates the isa scores:
	 * <p>
	 * isaHits 			The number of constants in common in the allIsa lists given by the two constants in the pair.
	 * isaInverseDist	The sum of path lengths of all constants in common for both lists of all isas.
	 * firstIsaDist		The distance between the first cyc constant found on both allIsa lists for the pair.
	 * matches			The list of cyc constants in common in each of the allIsa and allGenls lists of the pair.
	 * firstMatches		The list of first isa and first genls in common from the allIsa and allGenls lists of the pair.
	 *   
	 * @return true if the isa scores where calculated, false if either allIsa list is null 
	 * @see CycConnection#getAllIsa
	 * 	
	 */
	@SuppressWarnings("unchecked")
	public boolean calcIsaScores()
	{			
		CycList allIsa = this.cycConn.getAllIsa(c);
		CycList allIsa1 = this.cycConn.getAllIsa(c1);				
		if ((allIsa == null) || (allIsa1 == null))
			return false;
		else			
			if ((allIsa.size()>0)&&(allIsa1.size()>0))
				if ((allIsa.get(0).equals(c1)) || (allIsa1.get(0).equals(c)))
				{
					isaHits++;
					isaInverseDist += 1;		
					if (firstIsaDist > 1)
						firstIsaDist = 1;
					if (allIsa.get(0).equals(c1))
					{
						matches.add(allIsa.get(0));
						if (firstIsaDist > 1)				
							firstMatches.add(allIsa.get(0));							
					}
					else if (allIsa1.get(0).equals(c))
					{
						matches.add(allIsa1.get(0));
						if (firstIsaDist > 1)
							firstMatches.add(allIsa1.get(0));
					}
				}

		for (int i = 0; i < allIsa.size(); i++)
		{									
			for (int j = 0; j < allIsa1.size(); j++)
			{
				if (allIsa.get(i).equals(allIsa1.get(j)))
				{
					isaHits++; isaInverseDist += j+i+2; matches.add(allIsa.get(i));
					if (i+j+2 < firstIsaDist){firstIsaDist=j+i+2; firstMatches.add(allIsa.get(i));}
					//Log.current.println("isaHits match hits- i: " + i + "; j: " + j);
				}		
			}
		}
		return true;
	}
	/**
	 * @return 	the number of constants in common by comparing linearly: 
	 * 				the lists given by the OpenCyc world corresponding to all 
	 * 				genls relationships of the two constants in this pair.
	 * @see CycConnection#getAllGenls
	 * 				
	 */	
	public int getGenlsHits()
	{
		return this.genlsHits;
	}
	
	/**
	 * 
	 * @return the sum of the path lengths for every constant found on both lists of all genls.
	 */
	public int getGenlsInverseDist()
	{
		return this.genlsInverseDist;
	}
	/**
	 * Calculates the genls scores:
	 * <p>
	 * genlsHits 			The number of constants in common in the allGenls lists given by the two constants in the pair.
	 * genlsInverseDist		The sum of path lengths of all constants in common for both lists of all genls.
	 * firstGenlsDist		The distance between the first cyc constant found on both allIsa lists for the pair.
	 * matches				The list of cyc constants in common in each of the allIsa and allGenls lists of the pair.
	 * firstMatches			The list of first isa and first genls in common from the allIsa and allGenls lists of the pair.
	 *   
	 * @return true if the isa scores where calculated, false if either allIsa list is null 
	 * @see CycConnection#getAllIsa
	 * 	
	 */
	@SuppressWarnings("unchecked")
	public boolean calcGenlsScores()
	{			
		CycList allGenls = this.cycConn.getAllGenls(c);
		CycList allGenls1 = this.cycConn.getAllGenls(c1);				
		if ((allGenls == null) || (allGenls1 == null))
			return false;
		else
			if ((allGenls.size()>0)&&(allGenls1.size()>0))
				if ((allGenls.get(0).equals(c1)) || (allGenls1.get(0).equals(c)))
				{
					genlsHits++;
					genlsInverseDist += 1;
					if (firstGenlsDist > 1)
						firstGenlsDist=1;
					if (allGenls.get(0).equals(c1))
					{
						matches.add(allGenls.get(0));
						if (firstGenlsDist == 1)
							firstMatches.add(allGenls.get(0));
					}
					else if (allGenls1.get(0).equals(c))
					{
						matches.add(allGenls1.get(0));
						if (firstGenlsDist == 1)
							firstMatches.add(allGenls1.get(0));
					}
				}

		for (int i = 0; i < allGenls.size(); i++)
		{
			for (int j = 0; j < allGenls1.size(); j++)
			{
				if (allGenls.get(i).equals(allGenls1.get(j)))
				{
					genlsHits++; genlsInverseDist += j+i+2; matches.add(allGenls.get(i));
					if (j+i+2 < firstGenlsDist){firstGenlsDist=j+i+2; firstMatches.add(allGenls.get(i));}
					//Log.current.println("genls match hits- i: " + i + "; j: " + j);
				}
			}
		}
		return true;
	}


	/**
	 * @return sum of genlsHits and isaHits
	 */
	public int getTotalHits()
	{	//TODO add way to switch to just either one.
		int sum=0;
		sum+= this.genlsHits;		
		sum+= this.isaHits;		

		return sum;	
	}
	
	/**
	 *	@return		the sum of genlsInverseDist and isaInverseDist 
	 *				minus the total path length from constant 'Thing' (allIsa and allGenls of 'Thing')
	 *
	 * @see AbstractElementPair#genlsInverseDist
	 * @see AbstractElementPair#isaInverseDist
	 */
	public int getAllInverseDistance()
	{
		int sum=totalLevels;

		sum-= this.genlsInverseDist;		
		sum-= this.isaInverseDist;

		//Log.current.println(" " + sum + "- " + this.isaMatchValue + "= ");
		return sum;
	}

	/**
	 * @return the shortest first hit path distance from either scoring type: genls or isa.
	 * @see AbstractElementPair#firstGenlsDist
	 * @see AbstractElementPair#firstIsaDist
	 */
	public int getSmallestFirstDistance()
	{
		int smallest;
		smallest = AbstractElementPair.max_int;
		if (this.firstGenlsDist < smallest)
			smallest =this.firstGenlsDist;
		if (this.firstIsaDist < smallest)
			smallest = this.firstIsaDist;
		return smallest;
	}

	/**
	 * Calls all scoring methods for all relationship types included in this pair.
	 * 
	 * @see AbstractElementPair#calcGenlsScores
	 * @see AbstractElementPair#calcIsaScores
	 */
	public boolean calcCatSimScores()//TODO ADD SWITCH TO TOGGLE BETWEEN WHICH SCORES GET CALCULATED
	{
		this.init();
		return this.calcGenlsScores() || this.calcIsaScores();	
	}
	
	public boolean calcAngle() {
		this.initRel();				 
		CycList list = cycConn.getAllIsa(this.c);//TODO SORT THIS TO GET LOWEST TO HIGHEST!!
		//list.addAll(cycConn.getAllGenls(this.c));
		
		CycObject genlsIsa;

		for (int i =0; i < list.size();i++)
		{
			genlsIsa = (CycObject) list.get(i);
			this.v = this.createVector(genlsIsa);
			//System.out.println("Vector Created: "+ this.v);
			if (!this.v.isNaN())
			{
				this.l = genlsIsa;
				break;
			}
		}
		if (this.l == null) return false;
		
		list.clear();
		list = cycConn.getAllIsa(this.c1);
		//list.addAll(cycConn.getAllGenls(this.c1));
		
		for (int i =0; i < list.size();i++)
		{
			genlsIsa = (CycObject)list.get(i);
			this.v1 = this.createVector(genlsIsa);
			//System.out.println("Vector Created: "+ this.v1);
			if (!this.v1.isNaN())
			{
				this.l1 = genlsIsa;
				break;
			}
		}
		
		if ((this.l != null) && (this.l1 != null))
		{		
			if (this.v.equals(this.v1))
			{
				this.angle = 0.0;
				return true;
			}		
		//compare vectors from opposing pair members to calculate angle
/*			System.out.println("v: "+this.v);
			System.out.println("v1: "+this.v1);
			System.out.println("l: "+this.l);
			System.out.println("L1: "+this.l1);*/
			Double lengthv = Math.sqrt(this.v.dotProduct(this.v));
			//System.out.println("length of v: "+lengthv);
			Double lengthv1 = Math.sqrt(this.v1.dotProduct(this.v1));
			//System.out.println("length of v1: "+lengthv1);
			Double cosTheta = (this.v.dotProduct(this.v1))/(lengthv*lengthv1);
			//System.out.println("cosTheta: "+cosTheta);
			this.angle = Math.acos(cosTheta);
			//System.out.println("angle: "+angle);
			if (this.angle==null)
				return false;
			else
				return true;
		}
		else
			return false;
	}

	private ArrayRealVector createVector(CycObject lowestC) {
		Double [] nan = {Double.NaN};//nullifies a vector
		if (lowestC != null)
		{
			ArrayList<Double> results = new ArrayList<Double>();
			CycList r;
			CycVariable variable = CycObjectFactory.makeCycVariable("?p");
			//CycConstant mt = cycConn.findConstant("BaseKB");
	        CycConstant mt = cycConn.findConstant("InferencePSC");
	        //CycConstant mt = cycConn.findConstant("EverythingPSC");
	        //CycConstant mt = cycConn.findConstant("CurrentWorldDataCollectorMt-NonHomocentric");
	        
			CycList query = cycConn.makeCycList("(#$and (#$isa ?p #$Predicate) (#$arity ?p 2) (#$arg1Isa ?p "+ lowestC.cyclify()+"))" );		
			r = cycConn.queryVariable(query, variable, mt);
			results.add(Integer.valueOf(r.size()).doubleValue());			

			query = cycConn.makeCycList("(#$and (#$isa ?p #$Predicate) (#$arg2Isa ?p "+ lowestC.cyclify()+"))" );
			r = cycConn.queryVariable(query, variable, mt);
			results.add(Integer.valueOf(r.size()).doubleValue());
						
			query = cycConn.makeCycList("(#$and (#$isa ?p #$Predicate) (#$arg3Isa ?p "+ lowestC.cyclify()+"))" );
			r = cycConn.queryVariable(query, variable, mt);
			results.add(Integer.valueOf(r.size()).doubleValue());
			
			query = cycConn.makeCycList("(#$and (#$isa ?p #$Predicate) (#$arg4Isa ?p "+ lowestC.cyclify()+"))" );		
			r = cycConn.queryVariable(query, variable, mt);
			results.add(Integer.valueOf(r.size()).doubleValue());
			
			ArrayList<Double> copy = new ArrayList<Double>(results);
			while(copy.remove(new Double(0)));//check if all zeroes
			
			if (!copy.isEmpty())
			{
				Double [] freqs = new Double[results.size()];
				results.toArray(freqs);
				return new ArrayRealVector(freqs);
			}		
			else 
				return new ArrayRealVector(nan);
		}
		else
			return new ArrayRealVector(nan);
	}

	/**
	 *	Calculates all scores and prints them.
	 * 
	 * @see AbstractElementPair#calcCatSimScores
	 * 
	 */			
	public void printCatSimScores()
	{			
		if (this.calcCatSimScores())
		{
	
			Log.current.println("Printing all scores for pair: " + this.toString());	
			Log.current.println("genls Hits: " + this.genlsHits);
			Log.current.println("genls Path Distance: " + this.genlsInverseDist);				
	
			if (this.firstGenlsDist == max_int)
				Log.current.println("genls First Hit Distance: No Hits");
			else	
				Log.current.println("genls First Hit Distance: " + this.firstGenlsDist);
	
			Log.current.println("isa Hits: " + this.isaHits);
			Log.current.println("isa Path Distance: " + this.isaInverseDist);
	
			if (this.firstIsaDist == max_int)
				Log.current.println("isa First Distance: No Hits");
			else
				Log.current.println("isa First Distance: " + this.firstIsaDist);
			Log.current.println("*** Synthesized output: ***");
			Log.current.println("Total Hits: " + this.getTotalHits());
			Log.current.println("Inverse Distances: " + this.getAllInverseDistance());
			Log.current.println("Smallest First Distance: " + this.getSmallestFirstDistance());
		}
		else
			Log.current.println("ERROR - Score calculation failed");			
	}
	
	public void printRelSimScores()
	{			
		if (this.calcAngle())
		{
	
			Log.current.println("Printing angles for pair: " + this.toString());	
			Log.current.println("Vector Category for element 1: " + this.l);
			Log.current.println("Vector Category for element 2: " + this.l1);
			Log.current.println("Vector for element 1: " + this.v);
			Log.current.println("Vector for element 2: " + this.v1);
			Log.current.println("Angle between vectors: " + this.angle);
	
		}
		else
			Log.current.println("ERROR - Angle calculation failed");			
	}
	
	/**
	 * @see java.lang.Object#toString()
	 * @return the string representing this element pair - format : { constant, constant1 }
	 */	
	@Override		
	public String toString()
	{
		String self = ("{" + this.c + ", " + this.c1 + "}");
		return self;
	}
	
	public static String toString(CycConstant c, CycConstant c1)
	{
		String self = ("{" + c + ", " + c1 + "}");
		return self;
	}
	
	/**
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((c == null) ? 0 : c.hashCode());
		result = prime * result + ((c1 == null) ? 0 : c1.hashCode());
		return result;
	}

	/**
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (!(obj instanceof AbstractElementPair))
			return false;
		AbstractElementPair other = (AbstractElementPair) obj;
		
		if ((c != null) && (c1 != null))
		{
			if ((other.c != null) && (other.c1!= null))
			{
				if ((c.equals(other.c1)) && (c1.equals(other.c)))
					return true;
			}
		}
		
		if (c == null) 
		{
			if (other.c != null)
				return false;
		} 
		else if (!c.equals(other.c))
			return false;
		
		if (c1 == null) 
		{
			if (other.c1 != null)
				return false;
		} 
		else if (!c1.equals(other.c1))
			return false;
		
		return true;
	}
	/**
	 * 
	 * @return the list of isa and genls matches between the two lists given by the getAll methods for the pair
	 * @see AbstractElementPair#calcCatSimScores
	 */
	public CycList getMatches()
	{
		return this.matches;		
	}

	/**
	 * 
	 * @return the list of the first hit between getAllIsa of the two constants in the pair and the first hit
	 * between getAllgenls of the two constants.
	 */
	public CycList getFirstMatches()
	{
		return this.firstMatches;		
	}

	public Double getAngle() {
		if (this.angle == null)
			return Double.NaN;
		else
			return this.angle;
	}

}
